﻿
using S7Cat.Enums;
using S7Cat.Models;
using S7Cat.Protocol;
using S7Cat.Tools;
using System.Buffers;

namespace S7Cat
{
    public sealed class PooledPlc : BasePlc, IDisposable
    {
        internal bool IsReturned { get; set; }
        internal ConnectionPool Pool { get; init; }
        internal PooledPlc(CpuType cpu, string ip, int port, short rack, short slot, ConnectionPool pool) : base(cpu, ip, port, rack, slot)
        {
            Pool = pool;
        }

        /// <summary>
        /// 使用内存池缓存，读取字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        internal override async Task<MemoryOwner<byte>> ReadBytesPooledAsync(DataItemAddress address, CancellationToken cancellationToken = default)
        {
            var maxReadSize = MaxPDU - 18;
            var buffer = MemoryOwner<byte>.Allocate(address.ByteCount);
            if (address.ByteCount > maxReadSize)
            {
                Dispose();

                var count = address.ByteCount / maxReadSize;
                if (address.ByteCount % maxReadSize != 0)
                {
                    count++;
                }

                var ranges = ArrayPool<Range>.Shared.Rent(count);
                try
                {
                    var offset = 0;
                    for (int i = 0; i < count; i++)
                    {
                        var len = Math.Min(address.ByteCount - offset, maxReadSize);
                        ranges[i] = new Range(offset, offset + len);
                        offset += len;
                    }

                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        await plc.ReadBytesSplitAsync(address.Split(ranges[i]), buffer.Memory[ranges[i]], token).ConfigureAwait(false);
                    });
                }
                finally
                {
                    ArrayPool<Range>.Shared.Return(ranges);
                }
            }
            else
            {
                await ReadBytesSplitAsync(address, buffer.Memory, cancellationToken).ConfigureAwait(false);
            }

            return buffer;
        }

        /// <summary>
        /// 读取字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task<byte[]> ReadBytesAsync(DataItemAddress address, CancellationToken cancellationToken = default)
        {
            var maxReadSize = MaxPDU - 18;
            var buffer = new byte[address.ByteCount];

            if (address.ByteCount > maxReadSize)
            {
                Dispose();

                var count = address.ByteCount / maxReadSize;
                if (address.ByteCount % maxReadSize != 0)
                {
                    count++;
                }

                var ranges = ArrayPool<Range>.Shared.Rent(count);
                try
                {
                    var offset = 0;
                    for (int i = 0; i < count; i++)
                    {
                        var len = Math.Min(address.ByteCount - offset, maxReadSize);
                        ranges[i] = new Range(offset, offset + len);
                        offset += len;
                    }
                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        await plc.ReadBytesSplitAsync(address.Split(ranges[i]), buffer.AsMemory(ranges[i]), token).ConfigureAwait(false);
                    });
                }
                finally
                {
                    ArrayPool<Range>.Shared.Return(ranges);
                }
            }
            else
            {
                await ReadBytesSplitAsync(address, buffer, cancellationToken).ConfigureAwait(false);
            }

            return buffer;
        }

        /// <summary>
        /// 批量读取
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        internal override async Task<byte[]> ReadMultipleAsync(Memory<DataItemAddress> addresses, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)(Math.Sqrt(addresses.Length) * 2));
            try
            {
                var adrSpan = addresses.Span;
                var count = ConnectionRequest.SplitReadRequest(adrSpan, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }

                var totalSize = GetByteCount(adrSpan);
                var buffer = new byte[totalSize];

                var offset = 0;
                for (int i = 0; i < count; i++)
                {
                    var splitAdr = adrSpan[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    ranges[i + count] = new Range(offset, offset + size);
                    offset += size;
                }


                if (count == 1)
                {
                    using var request = ConnectionRequest.BuildSplitReadRequest(adrSpan[ranges[0]]);
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);
                    var span = data.Span;

                    if (span.Length < 15)
                    {
                        throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                    }

                    PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);
                    PackageParser.FillReadResponses(span, addresses.Span[ranges[0]], buffer.AsSpan(ranges[count]));
                }
                else
                {
                    Dispose();
                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var request = ConnectionRequest.BuildSplitReadRequest(addresses.Span[ranges[i]]);
                        ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        using var data = await plc.RequestTsduAsync(request.WrittenMemory, token).ConfigureAwait(false);
                        var span = data.Span;

                        if (span.Length < 15)
                        {
                            throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                        }

                        PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);
                        PackageParser.FillReadResponses(span, addresses.Span[ranges[i]], buffer.AsSpan(ranges[i + count]));
                    });
                }
                return buffer;
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }

        /// <summary>
        /// 批量读取
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override async Task<MultipleReader> ReadMultipleReaderAsync(Memory<DataItemAddress> addresses, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)(Math.Sqrt(addresses.Length) * 2));
            try
            {
                var adrSpan = addresses.Span;
                var count = ConnectionRequest.SplitReadRequest(adrSpan, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }

                var totalSize = GetByteCount(adrSpan);
                var buffer = ArrayPool<byte>.Shared.Rent(totalSize);

                var offset = 0;
                for (int i = 0; i < count; i++)
                {
                    var splitAdr = adrSpan[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    ranges[i + count] = new Range(offset, offset + size);
                    offset += size;
                }

                if (count == 1)
                {
                    using var request = ConnectionRequest.BuildSplitReadRequest(adrSpan[ranges[0]]);
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);

                    var span = data.Span;

                    if (span.Length < 15)
                    {
                        throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                    }

                    PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);

                    PackageParser.FillReadResponses(span, addresses.Span[ranges[0]], buffer.AsSpan(ranges[count]));
                }
                else
                {
                    Dispose();
                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var request = ConnectionRequest.BuildSplitReadRequest(addresses.Span[ranges[i]]);
                        ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        using var data = await plc.RequestTsduAsync(request.WrittenMemory, token).ConfigureAwait(false);

                        var span = data.Span;

                        if (span.Length < 15)
                        {
                            throw new Exception($"response error, request data : {Convert.ToHexString(span)}");
                        }

                        PackageParser.ValidateResponseCode((ReadWriteErrorCode)span[14]);
                        PackageParser.FillReadResponses(span, addresses.Span[ranges[i]], buffer.AsSpan(ranges[i + count]));
                    });
                }
                return new MultipleReader(addresses.Span, buffer, true);
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }

        /// <summary>
        /// 根据地址写入字节数组
        /// </summary>
        /// <param name="address"></param>
        /// <param name="bytes"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task WriteBytesAsync(DataItemAddress address, Memory<byte> bytes, CancellationToken cancellationToken = default)
        {
            var maxWrite = MaxPDU - 35;
            if (bytes.Length > maxWrite)
            {
                var count = address.ByteCount / maxWrite;
                if (address.ByteCount % maxWrite != 0)
                {
                    count++;
                }

                var ranges = ArrayPool<Range>.Shared.Rent(count);

                try
                {
                    var offset = 0;
                    for (int i = 0; i < count; i++)
                    {
                        var len = Math.Min(address.ByteCount - offset, maxWrite);
                        ranges[i] = new Range(offset, offset + len);
                        offset += len;
                    }

                    Dispose();
                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        await plc.WriteBytesSplitAsync(address.Split(ranges[i]), bytes[ranges[i]], token).ConfigureAwait(false);
                    });
                }
                finally
                {
                    ArrayPool<Range>.Shared.Return(ranges);
                }
            }
            else
            {
                await WriteBytesSplitAsync(address, bytes, cancellationToken).ConfigureAwait(false);
            }
        }

        /// <summary>
        /// 批量写入
        /// </summary>
        /// <param name="addresses"></param>
        /// <param name="dataBytes"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public override async Task WriteMultipleAsync(Memory<DataItemAddress> addresses, Memory<byte> dataBytes, CancellationToken cancellationToken = default)
        {
            var ranges = ArrayPool<Range>.Shared.Rent((int)(Math.Sqrt(addresses.Length) * 2));
            try
            {
                var adrSpan = addresses.Span;
                var count = ConnectionRequest.SplitWriteRequest(adrSpan, ranges, MaxPDU);

                if (count == 0)
                {
                    throw new Exception("splited requests is empty.");
                }


                var offset = 0;
                for (int i = 0; i < count; i++)
                {
                    var splitAdr = adrSpan[ranges[i]];
                    var size = GetByteCount(splitAdr);
                    ranges[i + count] = new Range(offset, offset + size);
                    offset += size;
                }

                if (count == 1)
                {
                    using var request = ConnectionRequest.BuildSplitWriteRequest(adrSpan[ranges[0]], dataBytes.Span[ranges[count]]);
                    ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                    using var data = await RequestTsduAsync(request.WrittenMemory, cancellationToken).ConfigureAwait(false);
                    var result = data.Span;
                    PackageParser.ValidateWriteResponses(result, addresses.Span[ranges[0]]);
                }
                else
                {
                    Dispose();
                    await Parallel.ForAsync(0, count, cancellationToken, async (i, token) =>
                    {
                        using var request = ConnectionRequest.BuildSplitWriteRequest(addresses.Span[ranges[i]], dataBytes.Span[ranges[i + count]]);
                        ConnectionRequest.ThrowIfRequestSizeTooLong(request.WrittenCount, MaxPDU);

                        using var plc = await Pool.GetAvailableAsync(token).ConfigureAwait(false);
                        using var data = await plc.RequestTsduAsync(request.WrittenMemory, token).ConfigureAwait(false);
                        var result = data.Span;
                        PackageParser.ValidateWriteResponses(result, addresses.Span[ranges[i]]);
                    });
                }
            }
            finally
            {
                ArrayPool<Range>.Shared.Return(ranges);
            }
        }

        public void Dispose()
        {
            if (IsReturned) return;

            IsReturned = true;
            Pool.Return(this);
        }
    }
}
